package net.kucoe.grin.context;

import java.lang.reflect.Field;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import net.kucoe.grin.Actor;
import net.kucoe.grin.Grin;
import net.kucoe.grin.internal.LimitedList;
import net.kucoe.grin.internal.interaction.*;
import net.kucoe.grin.internal.interaction.exception.InvocationFailedException;
import net.kucoe.grin.internal.interaction.exception.ParameterMissingException;
import net.kucoe.grin.utils.ValueHolder;
import net.kucoe.macarun.*;

/**
 * Describes wired {@link Actor}.
 * 
 * @author Vitaliy Basyuk
 */
public class ContextActor extends ContextWired {
    
    private static final Logger logger = Grin.getLogger("Actor");
    
    private final Map<String, ContextStrategy> strategies;
    private final Map<String, ContextParameter> parameters;
    private final ContextStrategy parametersLookupStrategy;
    
    /**
     * Constructs ContextActor.
     * 
     * @param wired
     * @param strategies
     * @param parameters
     * @param parametersLookupStrategy
     */
    public ContextActor(final Object wired, final Map<String, ContextStrategy> strategies,
            final Map<String, ContextParameter> parameters, final ContextStrategy parametersLookupStrategy) {
        super(wired);
        this.strategies = new HashMap<String, ContextStrategy>(strategies);
        this.parameters = new HashMap<String, ContextParameter>(parameters);
        this.parametersLookupStrategy = parametersLookupStrategy;
    }
    
    /**
     * Applies system strategy.
     * 
     * @param name
     * @param strategy
     */
    public void applyStrategy(final String name, final ContextStrategy strategy) {
        strategies.put(name, strategy);
    }
    
    /**
     * Returns the strategies Map<String,ContextStrategy>.
     * 
     * @return the strategies Map<String,ContextStrategy>.
     */
    public Map<String, ContextStrategy> getStrategies() {
        return strategies;
    }
    
    /**
     * Returns the parameters Map<String,ContextParameter>.
     * 
     * @return the parameters Map<String,ContextParameter>.
     */
    public Map<String, ContextParameter> getParameters() {
        return parameters;
    }
    
    /**
     * Returns the parametersLookupStrategy ContextStrategy.
     * 
     * @return the parametersLookupStrategy ContextStrategy.
     */
    public ContextStrategy getParametersLookupStrategy() {
        return parametersLookupStrategy;
    }
    
    /**
     * Starts interaction for result.
     * 
     * @param resultClazz
     * @param actor
     * @param validators
     * @return result
     */
    public <T> T interactFor(final Class<T> resultClazz, final ContextActor actor,
        final List<ContextValidator> validators) {
        final Map<String, Object> params = new HashMap<String, Object>();
        final Interaction<T> interaction = actor.induce(this, resultClazz, params, validators);
        interaction.addCallback(new InteractionCallback() {
            
            @Override
            public void onFail(final Throwable error) {
                interaction.signal();
            }
            
            @Override
            public void onContinue(final ContinueRequest request) {
                Object param = null;
                final ValueHolder<String> paramName = new ValueHolder<String>(null);
                Run<ParameterRelatedRequest, Object> run = new Run<ParameterRelatedRequest, Object>() {
                    public Object run(final ParameterRelatedRequest object) {
                        String name = object.getParameter();
                        paramName.set(name);
                        return paramsLookup(name);
                    }
                };
                Match match = Macarun.match(request);
                match.inCase(ParameterRelatedRequest.class).with("parameter", String.class).run(run).breaking();
                param = match.run();
                params.put(paramName.get(), param);
                interaction.interactWith(params);
            }
            
            @Override
            public void onComplete() {
                interaction.signal();
            }
        });
        interaction.await();
        return interaction.getResult();
    }
    
    /**
     * Returns whether actor produces result with type.
     * 
     * @param resultClazz
     * @return boolean
     */
    public boolean producable(final Class<?> resultClazz) {
        return findStrategy(resultClazz, null) != null;
    }
    
    /**
     * Induce actor for result
     * 
     * @param callee
     * @param resultClazz
     * @param params
     * @param validators
     * @return {@link Interaction}
     */
    public <T> Interaction<T> induce(final ContextActor callee, final Class<T> resultClazz,
        final Map<String, Object> params, final List<ContextValidator> validators) {
        final Interaction<T> interaction = new Interaction<T>(this, callee) {
            
            private List<ContextStrategy> skip = new LimitedList<ContextStrategy>(strategies.size());
            
            @Override
            public void onComplete() {
                logger.info("Interaction complete for " + resultClazz.getSimpleName());
            }
            
            @Override
            public void onFail(final Throwable error) {
                logger.log(Level.SEVERE, "Interaction failed for " + resultClazz.getSimpleName(), error);
            }
            
            public void interactWith(final Map<String, Object> params) {
                logger.info("Interaction initiated for " + resultClazz.getSimpleName());
                doWithStrategy(resultClazz, validators, params);
            }
            
            @SuppressWarnings("unchecked")
            private void doWithStrategy(final Class<T> resultClazz, final List<ContextValidator> validators,
                final Map<String, Object> params) {
                ContextStrategy strategy = findStrategy(resultClazz, skip);
                if (strategy != null) {
                    try {
                        T current = (T) strategy.invoke(params);
                        int validation = validate(validators, current);
                        if (validation <= 0) {
                            skip.add(strategy);
                            doWithStrategy(resultClazz, validators, params);
                            if (result == null) {
                                result = current;
                            }
                        }
                        complete();
                    } catch (ParameterMissingException e) {
                        doContinue(new ParameterNeededRequest(e.getParameter()));
                    } catch (InvocationFailedException e) {
                        doContinue(new ParameterFailedRequest(e.getParameter(), e.getCause()));
                    } catch (Exception e) {
                        fail(e);
                    }
                }
            }
            
        };
        interaction.interactWith(params);
        return interaction;
    }
    
    private Object paramsLookup(final String name) {
        ContextParameter contextParameter = parameters.get(name);
        if (contextParameter != null) {
            return getAndCatch(contextParameter.getField(), wired);
        }
        if (parametersLookupStrategy != null) {
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("name", name);
            return parametersLookupStrategy.invoke(map);
        }
        return null;
    }
    
    private ContextStrategy findStrategy(final Class<?> resultType, final List<ContextStrategy> skip) {
        for (ContextStrategy strategy : strategies.values()) {
            if (skip != null && skip.contains(strategy)) {
                continue;
            }
            Class<?> type = strategy.getMethod().getReturnType();
            if (resultType.isAssignableFrom(type)) {
                return strategy;
            }
        }
        return null;
    }
    
    private Object getAndCatch(final Field field, final Object object) {
        if (object != null) {
            try {
                return field.get(object);
            } catch (IllegalArgumentException e) {
                // skip
            } catch (IllegalAccessException e) {
                // skip
            }
        }
        return null;
    }
    
    private int validate(final List<ContextValidator> validators, final Object result) {
        int worstValidation = 0;
        for (ContextValidator validator : validators) {
            int validation = validator.validate(result);
            if (validation < worstValidation) {
                worstValidation = validation;
            }
        }
        return worstValidation;
    }
}
